昨天我們先建立了 config layer,今天實作真正的 datasource。
我還是會沿用前面的方式:start from the end。
與其一開始就先想 CSV reader 或 database connection,我會先決定 Agent 最後應該看到什麼。
目前我希望只提供兩個 datasource tools:
def read_file(filename):
...
def query_database(query):
...
從 Agent 的角度來看,這樣就夠了。
它只需要知道:
read_file()
query_database()
Agent 不需要知道 file 是怎麼 parse,也不需要知道 database connection 是怎麼建立的。
這些細節都留在 datasource implementation 裡。
整體 flow 會是:
Agent
↓
read_file / query_database
↓
Datasource implementation
這就是今天要完成的內容。
先從 CSV file 開始,Agent 最後應該可以這樣呼叫:
read_file("youtube_trending_cleaned_us.csv")
tool 大概長這樣:
def read_file(filename: str):
path = cfg.data.path.parent / filename
return CsvFileSource(path).read()
真正讀取 CSV 的邏輯則放在 datasource implementation:
from pathlib import Path
import pandas as pd
class CsvFileSource:
def __init__(self, path: Path):
self.path = path
def read(self) -> pd.DataFrame:
return pd.read_csv(self.path)
如果資料很大很多欄位的話,可以改用polars。
整個 flow 就會變成:
Agent
↓
read_file()
↓
CsvFileSource
↓
pandas.read_csv()
Agent 不需要知道 pandas.read_csv() 是怎麼運作的。
之後如果要支援 XLSX、PDF 或其他 file format,可以再補上對應的 implementation,read_file() 不需要跟著改變。
接著需要準備一個 database。
這個 demo 會使用 Docker Compose 在 local 啟動 PostgreSQL。
services:
postgresdb:
image: postgres:18
container_name: postgres_ithome
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5435:5432"
volumes:
- postgres_data:/var/lib/postgresql
volumes:
postgres_data:
PostgreSQL 預設通常使用 port 5432,不過我的 local machine 上 5432 已經被占用,所以這裡改成使用 5435。
5435:5432
代表:
localhost:5435 → PostgreSQL inside container:5432
credentials 放在 .env:
POSTGRES_USER=agent
POSTGRES_PASSWORD=agent
POSTGRES_DB=analytics
接著啟動 PostgreSQL:
docker compose up -d
這個 demo 也會把同一份 YouTube dataset 匯入 PostgreSQL。
今天不會花太多篇幅處理 data upload,因為這篇的重點是 Agent 要怎麼從不同 datasource 讀資料,而不是使用者怎麼把資料上傳進 system。
之後在做 frontend data-management flow 時,再把 file upload、backend 與 PostgreSQL 串起來。
對 database 來說,我希望 Agent 可以直接呼叫:
query_database(
"""
SELECT channel, SUM(views) AS total_views
FROM videos
GROUP BY channel
ORDER BY total_views DESC
LIMIT 10
"""
)
Agent 負責提供 query。
database connection 和 query execution 則留在 datasource implementation。
例如:
async def query_database(query: str):
source = PostgresSource(cfg.database.dsn)
return await source.query(query)
PostgreSQL implementation 可以像這樣:
import asyncpg
class PostgresSource:
def __init__(self, dsn: str):
self.dsn = dsn
async def query(self, sql: str) -> list[dict]:
conn = await asyncpg.connect(self.dsn)
try:
rows = await conn.fetch(sql)
return [dict(row) for row in rows]
finally:
await conn.close()
整個 flow:
Agent
↓
query_database()
↓
PostgresSource
↓
PostgreSQL
同樣地,Agent 不需要知道:
asyncpg.connect
database credentials
connection handling
query execution details
它只需要知道怎麼取得需要的資料。
雖然 file 和 database 都是 datasource,但它們本來就有不同的使用方式。
對 file 來說,直接讀取 dataset 很合理:
read_file(filename)
但 database 通常不需要整份讀進來。
Agent 可以只取得當前 Analysis 真正需要的資料:
query_database(query)
例如:
SELECT channel, SUM(views) AS total_views
FROM videos
GROUP BY channel
ORDER BY total_views DESC
LIMIT 10
而不是把整個 database 都載入 memory。
所以我不打算為了讓它們看起來一致,就強迫 file 和 database 使用完全一樣的方式。
它們可以有相同的目的,但不一定要有相同的 implementation。
目前 Agent 只會看到:
read_file
query_database
底下的 implementation 可以獨立變動。
例如之後可能會:
這些改動最好都留在 datasource implementation 裡,而不是一直影響 Agent。
核心想法就是:
Agent 應該看到它需要的能力,而不是底層 implementation details。
目前整體結構會變成:
Agent
↓
Tools
↓
Datasource implementation
↓
File / Database
對 file:
Agent
↓
read_file()
↓
CsvFileSource
↓
CSV
對 database:
Agent
↓
query_database()
↓
PostgresSource
↓
PostgreSQL
到這裡,Agent 已經有一個方式可以讀取 file 與 database。
下一步就可以真的讓 Agent 使用這些 tools,理解目前有哪些資料,並開始產生 analysis code。